透過上一節的文字說明,想在這節做一個簡單的示範。
我想完成從 MainActivity 輸入字串,按下按鈕後跳到 ResultActivity 並以 recycler view 顯示該字串和內容。
MainActivity:
ResultActivity:
因為有兩個畫面,所以會兩兩一起做介紹。
只有一個 edit text 跟兩個 button:一個功能是新增資料;另一個是切換至顯示資料的 ResultActivity
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainActivity">
<EditText
android:id="@+id/editMain"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.3"/>
<Button
android:id="@+id/btnMainSave"
android:text="save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/editMain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.2"/>
<Button
android:id="@+id/btnMainResult"
android:text="result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnMainSave"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.2"/>
</androidx.constraintlayout.widget.ConstraintLayout>
一個 recycler view 顯示內容
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.result.ResultActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listResult"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
還有它的 item
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/layoutItemResult"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textItemResultId"
android:text="0"
android:layout_margin="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/textItemResultContent"
android:text="content"
android:layout_margin="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/textItemResultPrint"
android:text="print"
android:layout_margin="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"/>
</LinearLayout>
<View
android:background="@android:color/darker_gray"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintTop_toBottomOf="@id/layoutItemResult"/>
</androidx.constraintlayout.widget.ConstraintLayout>
簡單的做完畫面,就可以開始了!
MVVM 有 三個部份,就先從 model 開始吧
這是上一篇講的 data class。我需要每次新增資料時會增加的 id,跟其內容,另外再加上一個方法給予我想要的字串
data class MainData(
var _id: Int,
var content: String,
) {
fun print()= "$_id:$content"
}
在這個範例中,我把資料存在 repository 裡面,並用靜態的概念把它們放在 object 裡面
class MainRepository {
companion object{
private var data= ArrayList<MainData>()
}
fun saveText(text: String){
data.add(MainData(data.size, text))
}
fun getAllText() = data
}
除此之外,還有兩個對於 data 操作的 function。
在 view model 裡面,會擁有它所需要的 repository 實體,再建立想要的方法並呼叫 repository 的方法。
因為我們有兩個畫面,所以也會需要有兩個 view model。
MainActivity 裡面是新增資料的。
class MainViewModel : ViewModel(){
private val repository= MainRepository()
fun saveText(text: String){
repository.saveText(text)
}
}
ResultActivity 裡面是顯示資料的。
class ResultViewModel: ViewModel(){
private val repository= MainRepository()
fun getAllText() = repository.getAllText()
}
除了 view model 自己本身,view model 還需要透過 factory 創造出實體,所以我在這裡,寫出 factory。
不過呢,其實是當 view model 有參數時,才要自製 view model factory,因為 如果採取一般的 ViewModelProvider
方式,view model 不能透過建構元傳入資料。但我習慣不管有沒有建構元,都會給他們 factory。
class MyViewModelFactory(): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.newInstance()
}
}
建立一個 class 實作 ViewModelProvider.Factory
。待會在使用 view model factory 的時候會傳入 view model 的 class 名,所以就利用泛型,將傳入的 class 做出實體並回傳。
這裡底下就是 activity 的內容了
class MainActivity : AppCompatActivity() {
private lateinit var edit: EditText
private lateinit var btnSave: Button
private lateinit var btnResult: Button
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this, MyViewModelFactory).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
edit= findViewById(R.id.editMain)
btnSave= findViewById(R.id.btnMainSave)
btnResult= findViewById(R.id.btnMainResult)
btnSave.setOnClickListener {
viewModel.saveText(edit.text.toString())
edit.text = null
}
btnResult.setOnClickListener {
startActivity(Intent(this, ResultActivity::class.java))
}
}
}
view model 以剛剛的 factory class 建立實體,有了 view model 以後,就在需要的地方呼叫 view model 的方法就好了
至於為什麼要使用加上 by lazy 修飾詞,試試看就知道了
當我們把 lazy 拔掉後 run app 的時候就會出問題了,開不起來
看一下 logcat:Caused by: java.lang.IllegalStateException: Your activity is not yet attached to the Application instance. You can't request ViewModel before onCreate call.
看來是不能在 onCreate() 呼叫前對 view model 操作。
這裡就要講一下除了 lateinit 的另一種延遲初始化的方法
lazy
他會在你要使用他的時候,才進行初始化,也就是整個跑完以後,等我按下 btnSave
按鈕才會開始初始化,藉此除掉錯誤。
就我的理解來說,lateinit 適用在 var;lazy 適用在 val
class ResultActivity : AppCompatActivity() {
private val viewModel: ResultViewModel by lazy {
ViewModelProvider(this, MyViewModelFactory).get(ResultViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_result)
val recyclerView: RecyclerView= findViewById(R.id.listResult)
recyclerView.apply {
adapter= ResultRecyclerAdapter(viewModel)
layoutManager= LinearLayoutManager(context)
}
}
}
因為 recycler view adapter 需要資料,就把 view model 當參數傳入即可。
要做出接收 view model 的建構元
class ResultRecyclerAdapter(
private val viewModel: ResultViewModel
): RecyclerView.Adapter<ResultRecyclerAdapter.ResultViewHolder>() {
class ResultViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
val textId: TextView= itemView.findViewById(R.id.textItemResultId)
val textContent: TextView= itemView.findViewById(R.id.textItemResultContent)
val textPrint: TextView= itemView.findViewById(R.id.textItemResultPrint)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
return ResultViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_result, parent, false))
}
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
Log.d(TAG, "onBindViewHolder: $position")
holder.textId.text= viewModel.getAllText()[position]._id.toString()
holder.textContent.text= viewModel.getAllText()[position].content
holder.textPrint.text= viewModel.getAllText()[position].print()
}
override fun getItemCount(): Int = viewModel.getAllText().size
}
其中有需要資料的地方,就可以跟 view model 拿了。
成果就像開頭的那兩張圖片一樣。